/*  CommandLineParser.java
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package drivechain;

import com.townleyenterprises.command.CommandListener;
import com.townleyenterprises.command.CommandOption;
import com.townleyenterprises.command.CommandParser;
import java.io.File;
import java.io.OutputStream;
import java.util.HashMap;
import tal.drivechain.chain.AbortChainException;
import tal.drivechain.chain.ChainController;
import tal.drivechain.chain.ChainNode.MSG;
import tal.drivechain.chain.script.ScriptCompiler;
import tal.drivechain.chain.shell.InfoPrinter;
import tal.drivechain.module.xmlconfig.LoggerInfo;
import tal.drivechain.plugin.Logger;
import tal.drivechain.plugin.Module;
import tal.drivechain.plugin.ModulePackageManager;
import tal.drivechain.system.ConfigManager;
import tal.drivechain.system.Globals;
import tal.drivechain.system.PluginLoader;

/**
 * Parses command line options and process them.
 *
 * @author eisental
 */
public class CommandLineParser implements CommandListener {

    final static CommandOption info = new CommandOption("info", 'v', false,
            "Sets the info flag in the root chain on.",
            "drivechain will display information messages while running.");
    final static CommandOption debug = new CommandOption("debug", 'b', false,
            "Sets the debug flag in the root chain on.",
            "drivechain will display data for debugging.");
    final static CommandOption timing = new CommandOption("timing", 't', false,
            "Sets the timing flag on in every logger.",
            "adds the time in miliseconds since the last log command was sent to the log.");
    final static CommandOption aboutmod = new CommandOption("aboutmod", 'g', true,
            "<module name>",
            "print info about using this module class");
    final static CommandOption listmodules = new CommandOption("listmodules", 'l', true,
            "<package name/all>",
            "lists all available modules in the specified package or all the modules in the system if used with --listmodules all");
    final static CommandOption listloggers = new CommandOption("listloggers", 'e', true,
            "<package name/'all'",
            "lists all available loggers in the specified package or all the loggers in the system used with --listloggers all");
    final static CommandOption listpackages = new CommandOption("listpackages", 'k', false,
            "get packages list",
            "lists installed modules packages");
    final static CommandOption aboutscript = new CommandOption("aboutscript", 's', true,
            "<script filename>",
            "prints info about a script file");
    final static PluginOption logger = new PluginOption("logger", 'a', "<logger name>{<properties>}",
            "drivechain will run this logger and send messages to it.");
    final static CommandOption[] opts = {debug, aboutscript, aboutmod, listmodules, listloggers, info, timing, logger, listpackages};

    private ConfigManager conf;
    private ChainController controller;
    private OutputStream out;
    private boolean timingFlag = false;
    private ChainCommandLineParser chainCommands;

    /**
     * Initialize the command parsers and parse arguments.
     *
     * @param args An array of arguments to parse.
     * @param conf A loaded ConfigManager object (conf.setConfigDefaults needs to be called).
     * @param out An output stream to log events and errors to.
     */
    public CommandLineParser(String[] args, ConfigManager conf, OutputStream out) {
        CommandParser clp = new CommandParser(Globals.VERSION.toString());
        chainCommands = new ChainCommandLineParser();
        clp.addCommandListener(this);
        clp.addCommandListener(chainCommands);
        clp.setExitOnMissingArg(true, -100);
        clp.setAbortExecuteOnError(true);
        clp.parse(args);
        this.conf = conf;
        this.out = out;

        // looks for unrecognized arguments and warns about it. System.out is used
        // since the logger is not running yet.
        String[] largs = clp.getUnhandledArguments();
        for (int i = 0; i < largs.length; ++i) {
            System.out.println("Unknown command-line argument: " +
                    largs[i]);
        }

        if (largs.length > 0) {
            System.exit(1);
        }
        
        try {
            if (debug.getMatched())
                setDebug();
            if (info.getMatched())
                setInfo();
            if (timing.getMatched())
                timingFlag = true;
            makeLoggers();

            if (aboutmod.getMatched()) {
                aboutModule(); System.exit(0);
            } else if (aboutscript.getMatched()) {
                aboutScript(); System.exit(0);
            } else if (listloggers.getMatched()) {
                listLoggers(); System.exit(0);
            } else if (listmodules.getMatched()) {
                listModules(); System.exit(0);
            } else if (listpackages.getMatched()) {
                listPackages(); System.exit(0);
            } 
        } catch (IllegalArgumentException e) { log(e.getMessage(), MSG.ERROR); System.exit(0); }
    }

    /**
     * Returns all the known command options that the app can handle.
     */
    public CommandOption[] getOptions() {
        return opts;
    }

    /**
     * Does nothing
     *
     * @param opt
     * @param arg
     */
    public void optionMatched(CommandOption opt, String arg) {}

    /**
     * Returns the description of the application that's displayed when any help screen is printed.
     * uses "[Globals.VERSION] options"
     */
    public String getDescription() {
        return Globals.VERSION + " options";
    }

    void createChain() throws AbortChainException {
        chainCommands.createChain(); 
    }

    /**
     *
     * @return the chain controller (a type of logger. Only one controller can be loaded).
     */
    public ChainController getChainController() {
        return controller;
    }

    /**
     * Prints info about the module that is named in the aboutmod argument.
     */
    private void aboutModule() {
        try {
            Module plg = PluginLoader.getModule(aboutmod.getArg());
            String help = InfoPrinter.printModuleAbout(plg);
            log(help, Module.MSG.ANOUNCE);
            return;
        } catch (IllegalArgumentException e) {
            log(e.getMessage(), Module.MSG.ERROR);
            return;
        }
    }

    /**
     * Prints info about the script that's named in the aboutscript argument.
     */
    private void aboutScript() {
        ScriptCompiler script = new ScriptCompiler();
        try {
            script.loadFromFile(ScriptCompiler.getScriptFile(aboutscript.getArg(), new File(System.getProperty("user.dir")), false));
            script.parseScript();
        } catch (IllegalArgumentException ie) {
            log(ie.getMessage(), Module.MSG.ERROR);
        }

        if (script.compileMetaData().isEmpty()) {
            log("No help is available for " + aboutscript.getArg(), Module.MSG.ANOUNCE);
        } else {
            log("\n==" + aboutscript.getArg() + "======\n", Module.MSG.ANOUNCE);
            for (String name : script.compileMetaData().keySet()) {
                log(name + ": " + script.compileMetaData().get(name), Module.MSG.ANOUNCE);
            }
        }

    }

    /**
     * Lists loggers in a specific package or in all packages according to the argument of listloggers.
     */
    private void listLoggers() {
        String pack;
        if (listloggers.getArg().equals("all"))
            pack = null;
        else pack = listloggers.getArg();
        HashMap<String, LoggerInfo> loggers = PluginLoader.getAvailableLoggers(pack);
        if (!loggers.isEmpty()) {
            log("The following loggers are available:\n", Module.MSG.ANOUNCE);
            for (String name : loggers.keySet()) {
                LoggerInfo loginfo = loggers.get(name);
                String description = loginfo.getDescription();
                log("\t* " + name + " - " + description, Module.MSG.ANOUNCE);
            }
        } else {
            log("There are no loggers in package " + listloggers.getArg(), Module.MSG.ANOUNCE);
        }
    }

    /**
     * Lists modules in a specific package or in all packages according to the argument of listmodules.
     */
    private void listModules() {
        String pack = listmodules.getArg();
        log("The following modules are available:\n", Module.MSG.ANOUNCE);
        log(InfoPrinter.printAvailableModules(pack), Module.MSG.ANOUNCE);
        log("\nTo get help on a specific module use --modulehelp <modulename>", Module.MSG.ANOUNCE);
    }

    /**
     * Lists currently loaded module packages.
     */
    private void listPackages() {
        log("", Module.MSG.ANOUNCE);
        log("Currently installed module packages:", Module.MSG.ANOUNCE);
        log("", Module.MSG.ANOUNCE);
        for (ModulePackageManager manager : PluginLoader.getPackageManagers().values()) {
            String name = manager.getPackageConfiguration().getPackageInfo().getName();
            String version = manager.getPackageConfiguration().getPackageInfo().getVersion().toString(false, true, true, false);
            String description = manager.getPackageConfiguration().getPackageInfo().getDescription();
            log("  * " + name + " " + version + " - " + description, Module.MSG.ANOUNCE);
        }
    }

    /**
     * Adds a logger to the root chain.
     *
     * @param name Logger name.
     * @param properties Logger properties in the format of name:value;...;name:value
     */
    private void loadLogger(String name, String properties) {
        Logger l = PluginLoader.getLogger(name);
        if (l instanceof ChainController) {
            if (controller != null) {
                throw new IllegalArgumentException("Can't have more than one chain controller (" + controller.getClass().getCanonicalName() + ", " + l.getClass().getCanonicalName());
            } else {
                controller = (ChainController) l;
            }
            controller.parseProperties(properties);
        }
        l.init(timingFlag, out);
        Globals.getRootChain().addLogger(l);
    }

    /**
     * Sets the root chains debug flag to true;
     */
    private void setDebug() {
        Globals.getRootChain().setDebugFlag(true);
    }

    /**
     * Sets the root chains info flag to true;
     */
    private void setInfo() {
        Globals.getRootChain().setInfoFlag(true);
    }

    /**
     * Create Logger obect according to the logger options in the commandline.
     */
    private void makeLoggers() {
        if (!logger.getMatched()) {
            String loggerName = conf.getDefaultLogger();
            if (loggerName==null)
                throw new IllegalArgumentException("A default logger is not specified. Please select one using --logger.");
            loadLogger(conf.getDefaultLogger(), null);
        } else {
            for (int i = 0; i < logger.getArgs().size(); i++) {
                String props = logger.getProperties(i);
                if (props!=null)
                    loadLogger(logger.getName(i), logger.getProperties(i));
            }
        }
    }

    /**
     * Logs a message in any way possible (uses every loaded logger or prints to screen in case no logger is available.
     * @param message A String that will be logged
     * @param msgType Type of log message.
     */
    void log(String message, Module.MSG msgType) {
        if (Globals.getRootChain().getLoggers().isEmpty())
            System.out.println(msgType.toString() + ": " + message);
        else {
            for (Logger l : Globals.getRootChain().getLoggers()) {
                l.generalMessage(message, msgType, "");
            }
        }
    }
}
